/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.apache.axis2.description;

import java.util.concurrent.ConcurrentHashMap;
import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNode;
import org.apache.axiom.om.OMText;
import org.apache.axis2.AxisFault;
import org.apache.axis2.engine.AxisConfiguration;
import org.apache.axis2.i18n.Messages;
import org.apache.axis2.modules.Module;
import org.apache.axis2.util.JavaUtils;
import org.apache.axis2.util.Utils;
import org.apache.axis2.wsdl.WSDLConstants;
import org.apache.axis2.wsdl.WSDLUtil;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.neethi.Assertion;
import org.apache.neethi.Policy;

import javax.xml.stream.XMLStreamException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public abstract class AxisDescription implements ParameterInclude,
		DescriptionConstants {

	protected AxisDescription parent = null;

	private ParameterInclude parameterInclude;

	private PolicyInclude policyInclude = null;


	private PolicySubject policySubject = null;

	private Map children;

	protected Map engagedModules;

	/** List of ParameterObservers who want to be notified of changes */
	protected List parameterObservers = null;

	private OMFactory omFactory = OMAbstractFactory.getOMFactory();

	// Holds the documentation details for each element
	private OMNode documentation;

	// creating a logger instance
	private static Log log = LogFactory.getLog(AxisDescription.class);

    public AxisDescription() {
        parameterInclude = new ParameterIncludeImpl();
        children = new ConcurrentHashMap();
        policySubject = new PolicySubject();
    }

	public void addParameterObserver(ParameterObserver observer) {
		if (parameterObservers == null)
			parameterObservers = new ArrayList();
		parameterObservers.add(observer);
	}

	public void removeParameterObserver(ParameterObserver observer) {
		if (parameterObservers != null) {
			parameterObservers.remove(observer);
		}
	}

	public void addParameter(Parameter param) throws AxisFault {
		if (param == null) {
			return;
		}

		if (isParameterLocked(param.getName())) {
			throw new AxisFault(Messages.getMessage("paramterlockedbyparent",
					param.getName()));
		}

		parameterInclude.addParameter(param);

		// Tell anyone who wants to know
		if (parameterObservers != null) {
			for (Iterator i = parameterObservers.iterator(); i.hasNext();) {
				ParameterObserver observer = (ParameterObserver) i.next();
				observer.parameterChanged(param.getName(), param.getValue());
			}
		}
	}

	public void addParameter(String name, Object value) throws AxisFault {
		addParameter(new Parameter(name, value));
	}

	public void removeParameter(Parameter param) throws AxisFault {
		parameterInclude.removeParameter(param);
	}

	public void deserializeParameters(OMElement parameterElement)
			throws AxisFault {

		parameterInclude.deserializeParameters(parameterElement);

	}

	/**
	 * If the parameter found in the current decription then the paremeter will
	 * be writable else it will be read only
	 * 
	 * @param name
	 * @return
	 */
	public Parameter getParameter(String name) {
		Parameter parameter = parameterInclude.getParameter(name);
		if (parameter != null) {
			parameter.setEditable(true);
			return parameter;
		}
		if (parent != null) {
			parameter = parent.getParameter(name);
			if (parameter != null) {
				parameter.setEditable(false);
			}
			return parameter;
		}
		return null;
	}

	public Object getParameterValue(String name) {
		Parameter param = getParameter(name);
		if (param == null) {
			return null;
		}
		return param.getValue();
	}

	public boolean isParameterTrue(String name) {
		Parameter param = getParameter(name);
		return param != null && JavaUtils.isTrue(param.getValue());
	}

	public ArrayList getParameters() {
		return parameterInclude.getParameters();
	}

	public boolean isParameterLocked(String parameterName) {

		if (this.parent != null && this.parent.isParameterLocked(parameterName)) {
			return true;
		}

		Parameter parameter = getParameter(parameterName);
		return parameter != null && parameter.isLocked();
	}

	public String getDocumentation() {
		if (documentation != null) {
			if (documentation.getType() == OMNode.TEXT_NODE) {
				return ((OMText) documentation).getText();
			} else {
				StringWriter writer = new StringWriter();
				documentation.build();
				try {
					documentation.serialize(writer);
				} catch (XMLStreamException e) {
					log.error(e);
				}
				writer.flush();
				return writer.toString();
			}
		}
		return null;
	}

	public OMNode getDocumentationNode() {
		return documentation;
	}

	public void setDocumentation(OMNode documentation) {
		this.documentation = documentation;
	}

	public void setDocumentation(String documentation) {
		if (!"".equals(documentation)) {
			this.documentation = omFactory.createOMText(documentation);
		}
	}

	public void setParent(AxisDescription parent) {
		this.parent = parent;
	}

	public AxisDescription getParent() {
		return parent;
	}

	/**
	 * @see org.apache.axis2.description.AxisDescription#setPolicyInclude(PolicyInclude)
	 * @deprecated As of release 1.4, if you want to access the policy cache of
	 *             a particular AxisDescription object use
	 *             {@line #getPolicySubject()} instead.
	 * 
	 * @param policyInclude
	 */
	public void setPolicyInclude(PolicyInclude policyInclude) {
		this.policyInclude = policyInclude;
	}


	/**
	 * @see org.apache.axis2.description.AxisDescription#getPolicySubject()
	 * @deprecated As of release 1.4, replaced by {@link #getPolicySubject()}
	 */
	public PolicyInclude getPolicyInclude() {
		if (policyInclude == null) {
			policyInclude = new PolicyInclude(this);
		}
		return policyInclude;
	}


    // NOTE - These are NOT typesafe!
    public void addChild(AxisDescription child) {
        if (child.getKey() == null) {
        // FIXME: Several classes that extend AxisDescription pass null in their getKey method.    
//            throw new IllegalArgumentException("Please specify a key in the child");
        } else {
            children.put(child.getKey(), child);
        }
    }



	public void addChild(Object key, AxisDescription child) {
		children.put(key, child);
	}

	public Iterator getChildren() {
		return children.values().iterator();
	}

    public AxisDescription getChild(Object key) {
        if(key == null) {
            // FIXME: Why are folks sending in null?
            return null;
        }
        return (AxisDescription) children.get(key);
    }

	public void removeChild(Object key) {
		children.remove(key);
	}

	/**
	 * This method sets the policy as the default of this AxisDescription
	 * instance. Further more this method does the followings. <p/> (1) Engage
	 * whatever modules necessary to execute new the effective policy of this
	 * AxisDescription instance. (2) Disengage whatever modules that are not
	 * necessary to execute the new effective policy of this AxisDescription
	 * instance. (3) Check whether each module can execute the new effective
	 * policy of this AxisDescription instance. (4) If not throw an AxisFault to
	 * notify the user. (5) Else notify each module about the new effective
	 * policy.
	 * 
	 * @param policy
	 *            the new policy of this AxisDescription instance. The effective
	 *            policy is the merge of this argument with effective policy of
	 *            parent of this AxisDescription.
	 * @throws AxisFault
	 *             if any module is unable to execute the effective policy of
	 *             this AxisDescription instance successfully or no module to
	 *             execute some portion (one or more PrimtiveAssertions ) of
	 *             that effective policy.
	 */
	public void applyPolicy(Policy policy) throws AxisFault {
		// sets AxisDescription policy
		getPolicySubject().clear();
		getPolicySubject().attachPolicy(policy);

		/*
		 * now we try to engage appropriate modules based on the merged policy
		 * of axis description object and the corresponding axis binding
		 * description object.
		 */
		applyPolicy();
	}

	/**
	 * Applies the policies on the Description Hierarchy recursively.
	 * 
	 * @throws AxisFault
	 *             an error occurred applying the policy
	 */
	public void applyPolicy() throws AxisFault {
		AxisConfiguration configuration = getAxisConfiguration();
		if (configuration == null) {
			return;
		}

		Policy applicablePolicy = getApplicablePolicy(this);
		if (applicablePolicy != null) {
			engageModulesForPolicy(this, applicablePolicy, configuration);
		}

		for (Iterator children = getChildren(); children.hasNext();) {
			AxisDescription child = (AxisDescription) children.next();
			child.applyPolicy();
		}
	}

	private boolean canSupportAssertion(Assertion assertion, List moduleList) {

		AxisModule axisModule;
		Module module;

		for (Iterator iterator = moduleList.iterator(); iterator.hasNext();) {
			axisModule = (AxisModule) iterator.next();
			// FIXME is this step really needed ??
			// Shouldn't axisMoudle.getModule always return not-null value ??
			module = axisModule.getModule();

			if (!(module == null || module.canSupportAssertion(assertion))) {
				log.debug(axisModule.getName() + " says it can't support "
						+ assertion.getName());
				return false;
			}
		}

		return true;
	}
	
	private void engageModulesForPolicy(AxisDescription axisDescription,
			Policy policy, AxisConfiguration axisConfiguration)
			throws AxisFault {
		/*
		 * for the moment we consider policies with only one alternative. If the
		 * policy contains multiple alternatives only the first alternative will
		 * be considered.
		 */
		Iterator iterator = policy.getAlternatives();
		if (!iterator.hasNext()) {
			throw new AxisFault(
					"Policy doesn't contain any policy alternatives");
		}

		List assertionList = (List) iterator.next();

		Assertion assertion;
		String namespaceURI;

		List moduleList;

		List namespaceList = new ArrayList();
		List modulesToEngage = new ArrayList();

		for (Iterator assertions = assertionList.iterator(); assertions
				.hasNext();) {
			assertion = (Assertion) assertions.next();
			namespaceURI = assertion.getName().getNamespaceURI();

			moduleList = axisConfiguration
					.getModulesForPolicyNamesapce(namespaceURI);

			if (moduleList == null) {
				log.debug("can't find any module to process "
						+ assertion.getName() + " type assertions");
				continue;
			}

			if (!canSupportAssertion(assertion, moduleList)) {
				throw new AxisFault("atleast one module can't support "
						+ assertion.getName());
			}

			if (!namespaceList.contains(namespaceURI)) {
				namespaceList.add(namespaceURI);
				modulesToEngage.addAll(moduleList);
			}
		}
		engageModulesToAxisDescription(modulesToEngage, this);
	}

	private void engageModulesToAxisDescription(List moduleList,
			AxisDescription description) throws AxisFault {

		AxisModule axisModule;
		Module module;

		for (Iterator iterator = moduleList.iterator(); iterator.hasNext();) {
			axisModule = (AxisModule) iterator.next();
			// FIXME is this step really needed ??
			// Shouldn't axisMoudle.getModule always return not-null value ??
			module = axisModule.getModule();

			if (!(module == null || description.isEngaged(axisModule.getName()))) {
				// engages the module to AxisDescription
				description.engageModule(axisModule);
				// notifies the module about the engagement
				axisModule.getModule().engageNotify(description);
			}
		}
	}

	public AxisConfiguration getAxisConfiguration() {

		if (this instanceof AxisConfiguration) {
			return (AxisConfiguration) this;
		}

		if (this.parent != null) {
			return this.parent.getAxisConfiguration();
		}

		return null;
	}

	public abstract Object getKey();


	/**
	 * Engage a Module at this level
	 * 
	 * @param axisModule
	 *            the Module to engage
	 * @throws AxisFault
	 *             if there's a problem engaging
	 */
	public void engageModule(AxisModule axisModule) throws AxisFault {
		engageModule(axisModule, this);
	}
	
    /**
     * Engage a Module at this level, keeping track of which level the engage was originally
     * called from.  This is meant for internal use only.
     *
     * @param axisModule module to engage
     * @param source the AxisDescription which originally called engageModule()
     * @throws AxisFault if there's a problem engaging
     */
    public void engageModule(AxisModule axisModule, AxisDescription source) throws AxisFault {
        if (engagedModules == null) engagedModules = new ConcurrentHashMap();
        String moduleName = axisModule.getName();
        for (Iterator iterator = engagedModules.values().iterator(); iterator.hasNext();) {
            AxisModule tempAxisModule = ((AxisModule) iterator.next());
            String tempModuleName = tempAxisModule.getName();

            if (moduleName.equals(tempModuleName)) {
                String existing = tempAxisModule.getVersion();
                if (!Utils.checkVersion(axisModule.getVersion(), existing)) {
                    throw new AxisFault(Messages.getMessage("mismatchedModuleVersions",
                            getClass().getName(),
                            moduleName,
                            existing));
                }
            }
            
        }

        // Let the Module know it's being engaged.  If it's not happy about it, it can throw.
        Module module = axisModule.getModule();
        if (module != null) {
            module.engageNotify(this);
        }

        // If we have anything specific to do, let that happen
        onEngage(axisModule, source);

        engagedModules.put(Utils.getModuleName(axisModule.getName(), axisModule.getVersion()),
                           axisModule);
    }
    
	protected void onEngage(AxisModule module, AxisDescription engager)
			throws AxisFault {
		// Default version does nothing, feel free to override
	}

	static Collection NULL_MODULES = new ArrayList(0);

	public Collection getEngagedModules() {
		return engagedModules == null ? NULL_MODULES : engagedModules.values();
	}

	/**
	 * Check if a given module is engaged at this level.
	 * 
	 * @param moduleName
	 *            module to investigate.
	 * @return true if engaged, false if not. TODO: Handle versions?
	 *         isEngaged("addressing") should be true even for versioned
	 *         modulename...
	 */
	public boolean isEngaged(String moduleName) {
		return engagedModules != null
				&& engagedModules.keySet().contains(moduleName);
	}

	public boolean isEngaged(AxisModule axisModule) {
		String id = Utils.getModuleName(axisModule.getName(), axisModule
				.getVersion());
		return engagedModules != null && engagedModules.keySet().contains(id);
	}

	public void disengageModule(AxisModule module) throws AxisFault {
		if (module == null || engagedModules == null)
			return;
		// String id = Utils.getModuleName(module.getName(),
		// module.getVersion());
		if (isEngaged(module)) {
			onDisengage(module);
			engagedModules.remove(Utils.getModuleName(module.getName(), module
					.getVersion()));
		}
	}

	protected void onDisengage(AxisModule module) throws AxisFault {
		// Base version does nothing
	}

	private Policy getApplicablePolicy(AxisDescription axisDescription) {
		if (axisDescription instanceof AxisMessage) {
			AxisMessage axisMessage = (AxisMessage) axisDescription;
			AxisOperation axisOperation = axisMessage.getAxisOperation();
			if (axisOperation != null) {
				AxisService axisService = (AxisService) axisOperation
						.getAxisService();
				if (axisService != null) {
					if (axisService.getEndpointName() != null) {
						AxisEndpoint axisEndpoint = axisService
								.getEndpoint(axisService.getEndpointName());
						if (axisEndpoint != null) {
							AxisBinding axisBinding = axisEndpoint.getBinding();
							AxisBindingOperation axisBindingOperation = (AxisBindingOperation) axisBinding
									.getChild(axisOperation.getName());
							String direction = axisMessage.getDirection();
							AxisBindingMessage axisBindingMessage = null;
							if (WSDLConstants.WSDL_MESSAGE_DIRECTION_IN
									.equals(direction)
									&& WSDLUtil
											.isInputPresentForMEP(axisOperation
													.getMessageExchangePattern())) {
								axisBindingMessage = (AxisBindingMessage) axisBindingOperation
										.getChild(WSDLConstants.MESSAGE_LABEL_IN_VALUE);
								return axisBindingMessage.getEffectivePolicy();
								
							} else if (WSDLConstants.WSDL_MESSAGE_DIRECTION_OUT
									.equals(direction)
									&& WSDLUtil
											.isOutputPresentForMEP(axisOperation
													.getMessageExchangePattern())) {
								axisBindingMessage = (AxisBindingMessage) axisBindingOperation
										.getChild(WSDLConstants.MESSAGE_LABEL_OUT_VALUE);
								return axisBindingMessage.getEffectivePolicy();
							}
						}

					}
				}
			}
			return ((AxisMessage) axisDescription).getEffectivePolicy();
		}
		return null;
	}

	public PolicySubject getPolicySubject() {
		return policySubject;
	}

}
